ARCH-001 Phase 8: MerchandiseReturn controller/service consolidation (Admin/Store/Vendor) - #799
Open
KrzysztofPajak wants to merge 17 commits into
Open
ARCH-001 Phase 8: MerchandiseReturn controller/service consolidation (Admin/Store/Vendor)#799KrzysztofPajak wants to merge 17 commits into
KrzysztofPajak wants to merge 17 commits into
Conversation
…turnDataScope per task review Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…inDataScope<MerchandiseReturn> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…andiseReturns storeId/vendorId, fold per-store customer-email lookup Admin/Store/Vendor's own MerchandiseReturnController.cs files still reference the old method name and will not build until Task 8's thin-subclass cutover replaces them - expected, not a regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
… region Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
… Admin/Store MerchandiseReturnController call sites Task 4 renamed IMerchandiseReturnViewModelService.PrepareReturnReqestListModel to PrepareReturnRequestListModel but left two callers on the old misspelled name, breaking the Grand.Web.Admin and Grand.Web.Store builds (and any test project depending on them). No behavioral change - these controllers are still replaced wholesale in Task 10. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…MerchandiseReturnController
…in/Vendor-throw vs Store-soft-deny seam Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…r to thin subclasses Deletes Vendor's drifted duplicate IMerchandiseReturnViewModelService/MerchandiseReturnViewModelService and its two model files, HasAccessToMerchandiseReturn (no remaining callers), and rebinds Vendor's views onto Grand.Web.AdminShared's model types via _ViewImports.cshtml aliases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
….Web.AdminShared, extract widget-zone satellites (store_merchandisereturn_* naming for Store) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…eturn removal regression guard, and SearchMerchandiseReturns storeId/vendorId forwarding test
…ibute tests - Created MerchandiseReturnControllerAttributeTests.cs with 5 tests - Verifies Admin, Store, and Vendor controllers all derive from BaseMerchandiseReturnController - Confirms base class has required PermissionAuthorizeAttribute for MerchandiseReturns - Confirms base class has AutoValidateAntiforgeryTokenAttribute - Uses extern alias for cross-host type references (StoreHost, VendorHost) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XGyrBT97Wr9zjxgDmG2KR
…n/Store/Vendor MerchandiseReturnController BaseMerchandiseReturnController only carries [PermissionAuthorize]+[AutoValidateAntiforgeryToken] (shared across hosts); each concrete host subclass must restate [Area]/[AuthorizeAdmin|Store|Vendor]/ [AuthorizeMenu] itself, same pattern already fixed for Order's identical Task-17 defect. Confirmed live: /Store/MerchandiseReturn/List 404'd before this fix (real Kestrel instance against the real dev DB), no longer 404s after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dd, drop dead orderId from notes AJAX payload, extend attribute regression guard to AuthorizeMenu Final whole-branch review finding: dropping the caller-supplied orderId parameter (an approved, intentional security fix) had silently also dropped the null-check on the server-resolved order, letting a note persist before a downstream NRE on a merchandise return whose order is missing - Admin/Store's original controllers both guarded against this. Restored. Also removes the now-unbound orderId field the shared view still posted, and extends the routing-attribute regression tests (added for the earlier live-found Area/Authorize bug) to cover AuthorizeMenu too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
||
| var view = result as ViewResult; | ||
| Assert.IsNotNull(view); | ||
| Assert.AreSame(model, view.Model); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("Edit", redirect.ActionName); |
|
|
||
| var json = result as JsonResult; | ||
| Assert.IsNotNull(json); | ||
| var gridModel = (DataSourceResult)json.Value; |
|
|
||
| var json = result as JsonResult; | ||
| Assert.IsNotNull(json); | ||
| var gridModel = (DataSourceResult)json.Value; |
| var result = await _controller.ProductsForMerchandiseReturn("mr1", new DataSourceRequest()); | ||
|
|
||
| var json = result as JsonResult; | ||
| var gridModel = (DataSourceResult)json.Value; |
|
|
||
| var redirect = result as RedirectToActionResult; | ||
| Assert.IsNotNull(redirect); | ||
| Assert.AreEqual("List", redirect.ActionName); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type: refactor
Issue
Grand.Web.Admin,Grand.Web.Store, andGrand.Web.Vendoreach shipped their ownMerchandiseReturnController— the same class of duplication ARCH-001 already fixed forProduct (PR #790), Category (PR #792), Collection (PR #794), Order (PR #795),
Shipment (PR #796), and PaymentTransaction (PR #797). This is the 8th and final scoped entity.
Full design:
docs/superpowers/specs/2026-08-26-arch001-merchandisereturn-consolidation-design.mdFull plan:
docs/superpowers/plans/2026-08-26-arch001-merchandisereturn-consolidation.md(Both gitignored under
docs/superpowers/, same as every prior phase — on disk in this branch'shistory but not tracked by git.)
MerchandiseReturnis a plainBaseEntitywith flatStoreId/VendorIdfields (notIStoreLinkEntity, not a child-collection vendor scan like Order) — the simplest scope shape inARCH-001 so far. All three hosts have an identical, full CRUD+notes action surface (Vendor is not
a restricted subset here, unlike Order) — a single-level
BaseMerchandiseReturnControllerwassufficient, no management/read split.
Solution
GlobalAdminDataScope<MerchandiseReturn>reused unmodified for Admin (the entity carries anunused
SeIdfield but, unlike Order, no live Sales-Manager restriction was ever built on it) +new bespoke
StoreMerchandiseReturnDataScope/VendorMerchandiseReturnDataScope(flat-fieldownership checks) + a 3-branch, fail-closed
RoutedMerchandiseReturnDataScope, all registeredcentrally in
Grand.Web.AdminShared/Startup/StartupApplication.cs.IMerchandiseReturnViewModelService/MerchandiseReturnViewModelServicehad driftedfrom
AdminShared's shared implementation (Product-Phase-1 shape, not Category/Collection's"Vendor never had one" shape) — missing attachment/
IDownloadServicesupport in notes, missingper-store-scoped customer-email lookup. Reconciled onto
AdminShared's service (adoptingVendor's per-store email-lookup behavior — a disclosed behavior change for Admin/Store, see
Breaking changes), Vendor's duplicate deleted outright along with its two model files and the
now-dead
HasAccessToMerchandiseReturnextension method.BaseMerchandiseReturnControllerinGrand.Web.AdminSharedholds all 9 actions(List, GoToId, ProductsForMerchandiseReturn, Edit ×2, Delete, and the 3 notes actions). One
genuine, deliberately preserved host divergence:
MerchandiseReturnNotesSelect'sAdmin/Vendor-throws-
ArgumentExceptionvs Store-soft-denies-with-empty-Content("")— expressedvia a
protected virtual NotFoundOrDeniedForNotesSelect()seam that Store's concrete subclassoverrides. (Written from scratch for this phase, not copied from Order's own
BaseOrderManagementController— that class turned out to have no equivalent host-differentiatedseam despite its own documentation implying one exists, a plan-authoring gap caught and corrected
during implementation, not carried forward here.)
MerchandiseReturnControllers reduced to thin subclasses (each restating itsown host's
[Area]/[Authorize*]/[AuthorizeMenu]attributes — see Testing for why this mattersmore than usual on this PR).
Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/(area-parameterized via the existing
ViewContext.RouteData.Values["area"]idiom), 11 widget-zonecall sites extracted into per-host satellites (Store's renamed
store_merchandisereturn_*/vc:store-widget, fixing the same "Store calledvc:admin-widget, a tag helper never registeredin the Store app" dead-markup bug already found and fixed in every prior phase). Vendor's 6 views
stay host-specific (missing Documents/UserFields tabs, plain-text customer field, no
attachment UI) — a deliberate decision against partial unification, matching Order's own
precedent for Vendor's narrower view shape.
superpowers:subagent-driven-development, task-by-task review ledgeravailable on request.
Breaking changes
None to any public method signature. Disclosed, deliberate behavior changes, all judged safe:
MerchandiseReturnNoteAdddrops the caller-suppliedorderIdrequest parameter entirely.Admin's original never validated that a caller-supplied
orderIdactually matched themerchandise return being noted; Store's original did validate it but only by comparing against
a value it should never have accepted from the client in the first place. The unified action now
always resolves
orderserver-side frommerchandiseReturn.OrderId— the strictest of the threeoriginal behaviors (matching Vendor's, which never took an
orderIdparameter at all), closing areal if low-severity gap.
CustomerConfig.RegisterCustomersPerStoreis enabled, searching the merchandise-return list bycustomer email is now scoped to the current store (matching Vendor's original behavior and every
other already-consolidated per-store lookup) instead of searching across all stores.
Delete'sModelState.IsValid == falsefallback branch removed. All three originals had anError(ModelState); return RedirectToAction("Edit", ...)branch for this case — practicallyunreachable since
Deletetakes only a routeidwith no bound form fields that can failvalidation. Consolidated version always redirects to
Liston this branch, consistent with"every access-denied redirect goes to List" elsewhere in this controller.
Testing
dotnet build GrandNode.sln— 0 errors, 4 pre-existing warnings unrelated to this diff (alreadypresent on
develop, none touching MerchandiseReturn files).dotnet test src/Tests/Grand.Web.Admin.Tests(unfiltered) — 683 passed, 0 failed.dotnet test src/Tests/Grand.Web.Store.Tests(unfiltered) — 42 passed, 0 failed.dotnet test src/Tests/Grand.Web.Vendor.Tests(unfiltered) — 12 passed, 0 failed.dotnet test src/Tests/Grand.Mapping.Tests(unfiltered) — 234 passed, 0 failed.MongoDB, with synthetic test data (4 merchandise returns covering a Store2-exclusive record, a
Store1-owned positive control, a "Vendor B"-exclusive record, and vendor1's own positive
control), driven as
store1@store.comandvendor1@vendor.com:thin
MerchandiseReturnControllersubclasses were missing[Area]/[Authorize*]/[AuthorizeMenu]— attributes that used to arrive transitively via each host's own basecontroller before consolidation, and which
BaseMerchandiseReturnController(shared acrosshosts) correctly can't carry itself. Every MerchandiseReturn route 404'd on all three hosts
until fixed. Exactly the failure class this live-test step exists to catch (unit tests
construct the controller directly, bypassing MVC routing entirely, so no test in this plan
could have caught it) — same bug class the Order phase's own Task 17 hit and fixed. Fixed,
reviewed, and independently live-re-verified (302, not 404) before the rest of the smoke test
ran.
GoToId/Edit(GET)/ProductsForMerchandiseReturn/MerchandiseReturnNoteAdd/MerchandiseReturnNoteDeleteallcorrectly deny cross-store access and succeed for Store1's own records;
MerchandiseReturnNotesSelecton a denied record returns HTTP 200 with an empty body, nota throw — confirming the preserved soft-deny divergence live, not just in unit tests.
Edit(GET) correctly denies adifferent vendor's record and succeeds for vendor1's own;
MerchandiseReturnNotesSelecton adenied record returns a genuine HTTP 500, server log confirming
System.ArgumentException: No merchandise return found with the specified idthrown fromNotFoundOrDeniedForNotesSelect()— confirming the Admin/Vendor-throw divergence live.Vendor's Edit view confirmed to show exactly 2 tabs (Info/Notes — no Documents/UserFields), no
raw untranslated resource-key strings.
phase); Admin's routing was independently confirmed live during the bug-fix re-verification
step above (302, not 404), and
GlobalAdminDataScope<MerchandiseReturn>is a triviallyalways-true implementation already covered by real (non-mocked) unit tests — judged the
lowest-risk gap to leave live-unverified, disclosed here rather than silently skipped.
NullReferenceExceptioninGrand.Business.Messages.Commands.Handlers.Messages.GetMerchandiseReturnTokensCommandHandler(unconditional
PickupAddress.CountryIddereference) when building a note-notification emailfor a merchandise return with no
PickupAddressset — reachable in real operation wheneverOrderSettings.MerchandiseReturns_AllowToSpecifyPickupAddressis disabled (it is, on this devDB), not just via this session's synthetic test data. Untouched business-layer code, no task in
this plan's diff goes near it — flagging for a separate fix, not blocking this PR.
superpowers:subagent-driven-development: 15 plan tasks + 1 critical live-foundbug fix + 1 final-review fix round (1 Important + 2 Minor addressed: a missing
order == nullguard in
MerchandiseReturnNoteAddthat could persist a note before a downstream 500 on amerchandise return whose order is missing — restored, matching Admin/Store's original strictest
behavior; a dead
orderIdfield removed from the shared notes view's AJAX payload; the routingregression tests extended to cover
[AuthorizeMenu]too, closing the gap the live-found bugopened) — all independently re-verified clean. Task-by-task review ledger available on request.
🤖 Generated with Claude Code